1、调试技术流程
具体调试流程如下:
1、对想钩取的进程进行附加操作,使之成为被调试者;
2、“钩子”:将API起始地址的第一个字节修改为0xCC;
3、调用相应API时,控制权转移到调试器;
4、执行需要的操作(操作参数、返回值等);
5、脱钩:将0xCC恢复原值;
6、运行相应API(无0xCC的正常状态);
7、“钩子”:再次修改为0xCC(为了继续钩取);
8、控制器返还被调试者。
以上是最简单的情形,在此基础上可以有多种变化。
2、记事本WriteFile() API钩取
首先运行Notepad.exe,获取其PID。
运行钩取程序(hookdbg.exe)。hookdbg.exe是基于控制台的程序,其运行参数为目标进程的PID,运行hookdbg.exe程序后,就开始了对notepad进程的WriteFile() API的钩取,如图所示。
然后在notepad中随意输入一下英文小写字母,如图所示。
完成输入后保存,notepad界面中不会有任何变化。关闭notepad,查看hookdbg程序的控制台窗口,如图所示。
打开保存的txt文件,查看实际文本是以大写字母形式保存。
3、工作原理
WriteFile()定义:
1 | BOOL WriteFile( |
使用OllyDbg打开notepad后,在Kernel32!WriteFile() API处设置断点,按(F9)键运行程序。在记事本中输入文本后,以合适的文件名保存,在OllyDbg代码窗口中可以看到,调试器在kernel32!WriteFile()处暂停,然后查看进程,发现当前栈中存在一个返回值,ESP+8中存在数据缓冲区的地址。直接转到数据缓冲区地址处,可以看到要保存到notepad的字符串。钩取WriteFile() API后,用指定字符串覆盖数据缓冲区中的字符串即可达成所愿。
4、源代码分析
1、main()
1 | int main(int argc, char * argv[]) |
main()函数以程序运行参数的形式接收要钩取API的进程的PID,然后通过DebugActiveProcess() API将调试器附加到该运行的进程上,开始调试。
1 | BOOL WINAPI DebugActiveProcess( |
然后进入DebugLoop()函数,处理来自被调试者的调试事件。
2、DebugLoop()
1 | void DebugLoop() |
DebugLoop()函数的工作原理类似于窗口过程函数,它从被调试者处接收事件并处理,然后被调试事件者继续运行。
WaitForDebugEvent() API是一个等待被调试者发生调试事件的函数。
1 | BOOL WINAPI WaitForDebugEvent( |
DebugLoop()函数代码中,若发生调试事件,WaitForDebugEvent() API就会将相关事件信息设置到其第一个参数的变量(DEBUG_EVENT结构体对像),然后立刻返回。
1 | typedef struct _DEBUG_EVENT { // de |
ContinueDebugEvent() API是一个使被调试者继续运行的函数。
1 | BOOL ContinueDebugEvent( |
DebugLoop()函数处理三种调试事件,如下所示。
1、EXIT_PROCESS_DEBUG_EVENT
2、CREATE_PROCESS_DEBUG_EVENT
3、EXCEPTION_DEBUG_EVENT
3、EXIT_PROCESS_DEBUG_EVENT
被调试进程终止时会插发该事件。
4、CREATE_PROCESS_DEBUG_EVENT(OnCreateProcessDebugEvent())
OnCreateProcessDebugEvent()是CREATE_PROCESS_DEBUG_EVENT事件句柄,被调试进程启动时即调用执行该函数。
1 | BOOL OnCreateProcessDebugEvent(LPDEBUG_EVENT pde) |
首先获取WriteFile() API的起始地址,它获取的不是被调试进程的内存地址,而是调试进程的内存地址。对于Windows OS的系统而言,它们在所有进程中都会加载到相同地址。
g_cpdi是CREATE_PROCESS_DEBUG_INFO结构体变量。
1 | typedef struct _CREATE_PROCESS_DEBUG_INFO { // cpdi |
通过CREATE_PROCESS_DEBUG_INFO结构体的hProcess成员,可以钩取WriteFile() API。
由于调试器拥有被调试进程的句柄,所以可以使用ReadProcessMemory()、WriteProcessMemory() API对被调试进程的内存空间自由进行读写操作。通过ReadProcessMemory()读取WriteFile() API的第一个字节,将其保存到g_chOrgByte变量,后面脱钩会用到。然后用WriteProcessMemory() API的第一个字节更改为0xCC,将控制权转移到调试器。
5、EXCEPTION_DEBUG_EVENT(OnExceptionDebugEvent())
OnExceptionDebugEvent()是EXCEPTION_DEBUG_EVENT事件句柄,它处理被调试者的INT3指令。
1 | BOOL OnExceptionDebugEvent(LPDEBUG_EVENT pde) |
1、“脱钩”
首先需要“脱钩”,在将小写字母转换为大写字母后需要正常调用WriteFile()函数。
1 | //Unhook,将0xCC恢复为original byte |
2、获取线程上下文
再次运行先前线程时,必须有运行所需的信息,这些重要信息指的就是CPU中各寄存器的值。通过这些值,才能保证CPU能够再次准确运行它。负责保存CPU寄存器信息的就是CONTEXT结构体,它的定义如下。
1 | typedef struct _CONTEXT |
下面是获取线程上下文的代码。
1 | ctx.ContextFlags = CONTEXT_CONTROL; |
像这样调用GetThreadContext() API,即可将指定线程的CONTEXT存储到ctx结构体变量。
1 | BOOL GetThreadContext( |
3、获取WriteFile()的param2、3的值
调用WriteFile()函数时,我们要在传递过来的参数中知道param2(数据缓冲区地址)与param3(缓冲区大小)这两个参数。通过CONTEXT.Esp成员可以分别获得它们的值。
1 | //获取WriteFile()的param 2、3值,param 2 = ESP + 0x8、param 3 = ESP + 0xC |
4、把小写字母转换为大写字母后覆写WriteFile()缓冲区
获取数据缓冲区的地址与大小后,将其内容读到调试器的内存空间,把小写字母转换为大写字母。然后将修改后的大写字母覆写到原位置。
1 | //分配临时缓冲区 |
5、把线程上下文的EIP修改为WriteFile()起始地址
修改好CONTEXT.Eip成员后,调用SetThreadContext() API。
1 | //将线程上下文的EIP更改为WriteFile()的首地址 |
6、运行调试进程
调用ContinueDebugEvent() API可以重启进程,使之继续运行。
7、设置API“钩子”
最后设置API“钩子”,方便下次钩取操作。